﻿#region 참조 네임스페이스

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using Skying.Diagnostics.Logger.Configuration;
using Skying.Diagnostics.Logger.Entry;

#endregion

namespace Skying.Diagnostics.Logger
{
	/// <summary>
	/// 로그 메시지를 기록하기 위한 로거를 제공한다.
	/// </summary>
	public sealed class Logger
	{
		#region 변수

		/// <summary>
		/// Logger에 대한 환경 설정을 나타낸다.
		/// </summary>
		/// <remarks>
		/// <para>환경 설정은 어플리케이션에 1:1로 대응한다.</para>
		/// </remarks>
		private static Configuration.Configurator _configuration;

		/// <summary>
		/// 타입별 로거 사전 목록을 나타낸다.
		/// </summary>
		/// <remarks>
		/// <para>이미 <see cref="Skying.Diagnostics.Logger"/>가 존재하는 타입을 통해 <see cref="Skying.Diagnostics.Logger.GetLogger"/>이 호출되는 경우 사전에서 <see cref="Skying.Diagnostics.Logger"/>를 가져와 사용한다.</para>
		/// </remarks>
		private static IDictionary<Type, Logger> _loggers;

		/// <summary>
		/// 로거에 대한 대상 타입을 나타낸다.
		/// </summary>
		private readonly Type _baseType;

		#endregion

		#region 프로퍼티

		/// <summary>
		/// 
		/// </summary>
		public Type BaseType
		{
			get
			{
				return this._baseType;
			}
		}

		#endregion

		#region 생성자/소멸자

		static Logger()
		{
			_loggers = new Dictionary<Type, Logger>();
		}

		private Logger(Type baseType)
		{
			this._baseType = baseType;
		}

		#endregion

		#region 환경 설정

		/// <summary>
		/// 로거의 환경 설정을 로드하여 로깅 가능한 상태로 둔다.
		/// </summary>
		public static void Configure()
		{
			_configuration = new Configurator();
		}

		/// <summary>
		/// <see cref="Skying.Diagnostics.Logger.Logger"/> 객체를 가져온다.
		/// </summary>
		/// <returns>설정된 타입에 대한 <see cref="Skying.Diagnostics.Logger.Logger"/>를 반환한다.</returns>
		public static Logger GetLogger()
		{
			StackTrace stackTrace = new StackTrace();

			StackFrame stackFrame = stackTrace.GetFrame(2);

			MethodBase method = stackFrame.GetMethod();

			// 메서드가 호출된 지점에 대한 타입이 사전 목록에 존재하는 경우 로거 생성의 낭비를 막기 위해 로거 사전에서 로거를 가져온다.
			if (_loggers.ContainsKey(method.DeclaringType))
			{
				return _loggers[method.DeclaringType];
			}

			Logger logger = new Logger(method.DeclaringType);

			_loggers.Add(method.DeclaringType, logger);

			return logger;
		}

		#endregion

		#region 로깅

		/// <summary>
		/// 로그 메시지와 예외 정보를 기록한다.
		/// </summary>
		/// <param name="logSeverity">로그를 남기는 심각도를 설정한다.</param>
		/// <param name="log">로그 메시지를 설정한다.</param>
		/// <param name="exception"></param>
		private void Logging(LogSeverity logSeverity, object log, Exception exception = null)
		{
			try
			{
				if (!_configuration.Installed)
				{
					Console.WriteLine("[Logger] Logging can't be. Please check logger configuration.");

					return;
				}

				StackTrace stackTrace = new StackTrace(true);

				// StackTrace에서 2번째 프레임은 로깅 메서드를 호출한 지점의 스택 프레임을 나타낸다.
				StackFrame stackFrame = stackTrace.GetFrame(2);

				MethodBase method = stackFrame.GetMethod();

				DateTime now = DateTime.Now;

				bool decided = false;

				// Arbitration이나 Root의 엔트리 목록을 나타낸다.
				IList<string> entryNames = new List<string>();

				// Arbitration 목록에서 로깅이 발생한 위치(네임스페이스)와 일치하는 Arbitration을 찾아 로깅에 이용한다.
				for (int index = 0; index < _configuration.LogArbitrations.Count; index++)
				{
					Arbitration.Arbitration arbitration = _configuration.LogArbitrations[index];

					// 로깅이 발생한 위치가 Arbitration의 네임스페이스에 포함되고 로그 심각도가 Arbitration에서 허용하는 심각도일 경우 사용할 엔트리 목록을 가져온다.
					if (method.DeclaringType.FullName.StartsWith(arbitration.Namespace) &&
						logSeverity >= arbitration.LogSeverity)
					{
						entryNames = arbitration.Entries;

						decided = true;

						break;
					}
				}

				// Arbitration 목록 중 해당되는 Arbitration이 없는 경우 Root를 사용한다.
				if (!decided && logSeverity >= _configuration.RootArtbitration.LogSeverity)
				{
					entryNames = _configuration.RootArtbitration.Entries;

					decided = true;
				}

				if (decided)
				{
					foreach (LogEntry logEntry in _configuration.LogEntries)
					{
						if (!entryNames.Contains(logEntry.EntryName)) continue;

						if (exception == null)
						{
							logEntry.Log(log.ToString(), logSeverity, now, stackFrame);
						}
						else
						{
							logEntry.Log(log.ToString(), logSeverity, now, stackFrame, exception);
						}
					}
				}
			}
			catch (Exception error)
			{
				Console.WriteLine("[Logger] An error has occurred when logger process a logging message. Exception information is the follows.");
				Console.WriteLine("[Logger] {0}", error.Message);

				if (string.IsNullOrEmpty(error.StackTrace))
				{
					Console.WriteLine("[Logger] {0}", error.StackTrace);
				}
			}
		}

		/// <summary>
		/// <para>로그 메시지를 Trace한다.</para>
		/// <para>네임스페이스에 의해 제한되지 않는 이상 로그 메시지는 로깅된다.</para>
		/// </summary>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Trace(object log)
		{
			this.Logging(LogSeverity.Trace, log);
		}

		/// <summary>
		/// <para>로그 메시지를 Trace한다.</para>
		/// <para>네임스페이스에 의해 제한되지 않는 이상 로그 메시지는 로깅된다.</para>
		/// </summary>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Trace(string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Trace, string.Format(format, arguments));
		}

		/// <summary>
		/// <para>로그 메시지를 Trace한다.</para>
		/// <para>네임스페이스에 의해 제한되지 않는 이상 로그 메시지는 로깅된다.</para>
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Trace(Exception error, string log)
		{
			this.Logging(LogSeverity.Trace, log, error);
		}

		/// <summary>
		/// <para>로그 메시지를 Trace한다.</para>
		/// <para>네임스페이스에 의해 제한되지 않는 이상 로그 메시지는 로깅된다.</para>
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Trace(Exception error, string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Trace, string.Format(format, arguments), error);
		}

		/// <summary>
		/// <para>Debug 정보를 남기기 위해 로깅한다.</para>
		/// <para>Debugger가 연결되었는지 여부는 관련이 없다.</para>
		/// </summary>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Debug(object log)
		{
			this.Logging(LogSeverity.Debug, log);
		}

		/// <summary>
		/// <para>Debug 정보를 남기기 위해 로깅한다.</para>
		/// <para>Debugger가 연결되었는지 여부는 관련이 없다.</para>
		/// </summary>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Debug(string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Debug, string.Format(format, arguments));
		}

		/// <summary>
		/// <para>Debug 정보를 남기기 위해 로깅한다.</para>
		/// <para>Debugger가 연결되었는지 여부는 관련이 없다.</para>
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Debug(Exception error, string log)
		{
			this.Logging(LogSeverity.Debug, log, error);
		}

		/// <summary>
		/// <para>Debug 정보를 남기기 위해 로깅한다.</para>
		/// <para>Debugger가 연결되었는지 여부는 관련이 없다.</para>
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Debug(Exception error, string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Debug, string.Format(format, arguments), error);
		}

		/// <summary>
		/// <para>응용 프로그램의 정보를 남기기 위해 로깅한다.</para>
		/// <para>응용 프로그램 상태나 진행 과정에 대한 기록을 남기기 위해 사용한다.</para>
		/// </summary>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Inform(object log)
		{
			this.Logging(LogSeverity.Information, log);
		}

		/// <summary>
		/// <para>응용 프로그램의 정보를 남기기 위해 로깅한다.</para>
		/// <para>응용 프로그램 상태나 진행 과정에 대한 기록을 남기기 위해 사용한다.</para>
		/// </summary>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Inform(string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Information, string.Format(format, arguments));
		}

		/// <summary>
		/// <para>응용 프로그램의 정보를 남기기 위해 로깅한다.</para>
		/// <para>응용 프로그램 상태나 진행 과정에 대한 기록을 남기기 위해 사용한다.</para>
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Inform(Exception error, string log)
		{
			this.Logging(LogSeverity.Information, log, error);
		}

		/// <summary>
		/// <para>응용 프로그램의 정보를 남기기 위해 로깅한다.</para>
		/// <para>응용 프로그램 상태나 진행 과정에 대한 기록을 남기기 위해 사용한다.</para>
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Inform(Exception error, string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Information, string.Format(format, arguments), error);
		}

		/// <summary>
		/// 오류는 아니지만 문제가 될 수 있는 내용을 로깅한다.
		/// </summary>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Warn(object log)
		{
			this.Logging(LogSeverity.Warning, log);
		}

		/// <summary>
		/// 오류는 아니지만 문제가 될 수 있는 내용을 로깅한다.
		/// </summary>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Warn(string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Warning, string.Format(format, arguments));
		}

		/// <summary>
		/// 오류는 아니지만 문제가 될 수 있는 내용을 로깅한다.
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Warn(Exception error, string log)
		{
			this.Logging(LogSeverity.Warning, log, error);
		}

		/// <summary>
		/// 오류는 아니지만 문제가 될 수 있는 내용을 로깅한다.
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Warn(Exception error, string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Warning, string.Format(format, arguments), error);
		}

		/// <summary>
		/// 오류 내용을 로깅한다.
		/// </summary>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Error(object log)
		{
			this.Logging(LogSeverity.Error, log);
		}

		/// <summary>
		/// 오류 내용을 로깅한다.
		/// </summary>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Error(string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Error, string.Format(format, arguments));
		}

		/// <summary>
		/// 오류 내용을 로깅한다.
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Error(Exception error, string log)
		{
			this.Logging(LogSeverity.Error, log, error);
		}

		/// <summary>
		/// 오류 내용을 로깅한다.
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Error(Exception error, string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Error, string.Format(format, arguments), error);
		}

		/// <summary>
		/// 치명적인 오류 내용을 로깅한다.
		/// </summary>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Fatal(object log)
		{
			this.Logging(LogSeverity.Fatal, log);
		}

		/// <summary>
		/// 치명적인 오류 내용을 로깅한다.
		/// </summary>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Fatal(string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Fatal, string.Format(format, arguments));
		}

		/// <summary>
		/// 치명적인 오류 내용을 로깅한다.
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="log">로그 메시지를 설정한다.</param>
		public void Fatal(Exception error, string log)
		{
			this.Logging(LogSeverity.Fatal, log, error);
		}

		/// <summary>
		/// 치명적인 오류 내용을 로깅한다.
		/// </summary>
		/// <param name="error">로깅할 <see cref="System.Exception"/> 정보를 설정한다.</param>
		/// <param name="format">로그 메시지 포맷을 설정한다.</param>
		/// <param name="arguments">로그 메시지 포맷을 지정할 개체 배열을 설정한다.</param>
		public void Fatal(Exception error, string format, params object[] arguments)
		{
			this.Logging(LogSeverity.Fatal, string.Format(format, arguments), error);
		}

		#endregion
	}
}